Docker Image Secrets the Right Way

It is occasionally useful to pass sensitive information such as AWS credentials (.aws/credentials) or authentication secrets when building a Docker image. Regardless of the reason, this information is sensitive and care must be taken to ensure that it is not leaked to consumers of the Docker image.

Docker users make the mistake of passing sensitive information to their image via --build-args or as environment variables. This has resulted in thousands of leaked secrets in public Docker images.

The ARG Problem

It's tempting to pass secrets via --build-args to the docker build command. This seems like the correct approach because the secret isn't hard-coded in the Dockerfile. This is not what the ARG instruction is for and the issues caused by its use are easy to demonstrate.

Here is an example Dockerfile that demonstrates using the ARG instruction to set a secret for use in a curl command.

FROM alpine

RUN apk update && apk add curl
ARG MY_SECRET
RUN curl -u "user:$MY_SECRET" https://ifconfig.me

Building this image requires the --build-arg to set the MY_SECRET variable. For example: docker build . -t blogtest --build-arg MY_SECRET="abcdefg". Now the image will use the value of MY_SECRET in the curl command.

The issue here is, again, that the ARG instruction was not meant to handle secrets and store the variable in the Docker image's metadata. Viewing the Docker image's layer history reveals the value of the ARG variable MY_SECRET.

$ docker history blogtest
IMAGE          CREATED          CREATED BY                                      SIZE      COMMENT
d01dea2da844   17 minutes ago   RUN |1 MY_SECRET=abcdefg /bin/sh -c curl -u …   0B        buildkit.dockerfile.v0
<missing>      17 minutes ago   ARG MY_SECRET                                   0B        buildkit.dockerfile.v0
<missing>      17 minutes ago   RUN /bin/sh -c apk update && apk add curl # …   4.35MB    buildkit.dockerfile.v0
<missing>      6 weeks ago      /bin/sh -c #(nop)  CMD ["/bin/sh"]              0B        
<missing>      6 weeks ago      /bin/sh -c #(nop) ADD file:df538113122843069…   5.33MB 

BuildKit to the Rescue

Previous versions of Docker had no straightforward way of passing secrets when building an image. It took great care and attention to detail to ensure that no sensitive data remained in the image. The arrival of Docker Buildkit adds the --secret option which provides a secure mechanism for passing sensitive information to an image.

We can rewrite the above example using the new Buildkit options. This requires one change to the Dockerfile.

FROM alpine

RUN apk update && apk add curl
RUN --mount=type=secret,id=mysecret MY_SECRET=$(cat /run/secrets/mysecret ) \
  && curl -u "user:$MY_SECRET" https://ifconfig.me

We no longer need the ARG instruction. Instead, we use the --mount argument with the RUN instruction. This syntax mounts our secret to /run/secrets/ in a file named after the id option, in this case: mysecret. The content of that file is then stored in our variable, MY_SECRET, so that we can use it in our curl command.

We can take advantage of the updated Dockerfile by adding the new --secret argument to our build command. This example assumes we have set the environment variable with our secret.

export MY_SECRET=abcdefg
docker build . -t blogtest --secret id=mysecret,env=MY_SECRET

Issuing the docker history command no longer reveals our secret.

docker history blogtest
IMAGE          CREATED         CREATED BY                                      SIZE      COMMENT
ce5a8e6e0840   8 minutes ago   RUN /bin/sh -c MY_SECRET=$(cat /run/secrets/…   0B        buildkit.dockerfile.v0
<missing>      4 hours ago     RUN /bin/sh -c apk update && apk add curl # …   4.35MB    buildkit.dockerfile.v0
<missing>      6 weeks ago     /bin/sh -c #(nop)  CMD ["/bin/sh"]              0B        
<missing>      6 weeks ago     /bin/sh -c #(nop) ADD file:df538113122843069…   5.33MB

Success! No more leaked secrets. The image can now be safely distributed.

A Couple Secret Secrets

There are a couple of rules to keep in mind when working with the --mount argument in a Dockerfile.

The --secret argument of docker build also has options which, at the time of writing, are not well documented. One of the omissions is the source argument. The example found in the Docker documentation demonstrates the src=<filename> usage. But looking at the source code reveals that the env=<variable name> is also a possible option (as demonstrated in the above example).

Buildkit's secret handling is a great way to improve the security of your Docker images. Now is an excellent time to start adding it to your images. The feature will reduce the amount of leaked secrets and allow your ops teams to rest a bit more easily.